/*
 * Developer : Jack Matthews
 * Email: developer@droidprofessor.com
 * 
 * Copyright (C) 2010 http://droidprofessor.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.droidprofessor.licensegenerator.lib;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.SignedObject;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
import java.util.TimeZone;

import com.droidprofessor.licensegenerator.lib.Base64;
import com.droidprofessor.licensegenerator.lib.Feature;
import static com.droidprofessor.licensegenerator.lib.Constants.*;


/**
 * Loads a license file into memory. 
 * This class it not Android specific and may be used in other
 * environments such as J2ME
 * 
 * @author Jack Matthews
 *
 */
public class LicenseLoader {

	private LicenseLoader(){};


	/**
	 * @return an instance of ClientLicense
	 */
	public static CoreClientLicense getInstance() {
		return LicenseLoaderWorkerImpl.INSTANCE;
	}



	/**
	 * This class should be used on the client 'Android Application'
	 * <br/><br/>
	 * To Access use:<br/>
	 * ClientLicense auth = LicenseLoader.getInstance();
	 * <br/><br/>
	 * Before you do anything you <b>need to execute the init() method</b>
	 * which will load the appropriate files into memory (once).
	 * Executing init() more than once will waste memory and is usually
	 * useless.  The only time you might execute init() a second time 
	 * during the application life cycle is when the user downloads
	 * a new license and needs to re-authenticate.
	 * <br/><br/>
	 * 
	 * Required: 
	 * READ_PHONE_STATE permission in the android manifest
	 * <br/><br/>
	 * @author Jack Matthews
	 *
	 */
	enum LicenseLoaderWorkerImpl implements CoreClientLicense {
		INSTANCE;

//		private static final String TAG = "LicenseLoader";
		private Properties props = null; //holds the properties
		private PublicKey publicKey = null;
		private SignedObject signedObject = null;

		@Override
		public void init(byte[] base64PublicKey, byte[] base64SignedObject) {
			if(base64PublicKey == null || base64PublicKey.length == 0)
				throw new IllegalArgumentException("base64PublicKey has no bytes");
			if(base64SignedObject == null || base64SignedObject.length == 0) { 
				//Log.w(TAG,"base64SignedObject has no bytes"); 
				System.err.print("base64SignedObject has no bytes");
				return;
			}

			try {
				initPublicKey(base64PublicKey);
			} catch (IOException e) {
				e.printStackTrace();
				throw new IllegalStateException("unable to decode public key");
			}
			
			initSignedObject(base64SignedObject);
			loadPropertiesObj();

		}


		/**
		 * sets up the SignedObject for use
		 * 
		 * @param base64SignedObject
		 */
		private void initSignedObject(byte[] base64SignedObject) {
			try {
				byte[] signedObj =  Base64.decode(base64SignedObject);
				ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(signedObj));
				this.signedObject = (SignedObject) ois.readObject();
				ois.close();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		
		
		
		private void loadPropertiesObj() {
			try {
				this.props = (Properties)this.signedObject.getObject();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (Exception e) {
				e.printStackTrace();
				//Log.e(TAG, "Unable to load properties from license");
			}
		}



		/**
		 * Initialise the encoded public key into 
		 * a usable object
		 * 
		 * @param base64PublicKey
		 * @throws IOException 
		 */
		private void initPublicKey(byte[] base64PublicKey) throws IOException {
			try {
				byte[] pubk = Base64.decode(base64PublicKey);
				X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubk);
				KeyFactory factory = KeyFactory.getInstance(RSA,PROVIDER);
				this.publicKey = factory.generatePublic(keySpec);
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			} catch (NoSuchProviderException e) {
				e.printStackTrace();
			} catch (InvalidKeySpecException e) {
				e.printStackTrace();
			}
		}


		
		/**
		 * Verify that the signature actually came from you
		 * 
		 * @return true on success
		 */
		private boolean verifySignature() {
			boolean verified = false;
			
			try {
				Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
				verified = this.signedObject.verify(this.publicKey, sig);
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			} catch (NoSuchProviderException e) {
				e.printStackTrace();
			} catch (InvalidKeyException e) {
				e.printStackTrace();
			} catch (SignatureException e) {
				e.printStackTrace();
			} 

			return verified;
		}



//		/**
//		 * Gets the Device ID
//		 * 
//		 * @return Encrypted Device ID
//		 * 
//		 * @see Context#TELEPHONY_SERVICE
//		 * @see TelephonyManager#getDeviceId()
//		 */
//		public String getDeviceId(){
//			TelephonyManager telephonyManager = 
//				(TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
//
//			String deviceId = telephonyManager.getDeviceId();
//
//			//if using the emulator make a fake one
//			if(TextUtils.isEmpty(deviceId))
//				deviceId = "123456789";
//
//			return doSha1AndBase64Encryption(
//					deviceId,
//					salt
//			);
//		}
//
//
//
//		/**
//		 * Gets the phone number for this device.
//		 * 
//		 * @return Encrypted phonenumber
//		 * 
//		 * @see Context#TELEPHONY_SERVICE
//		 * @see TelephonyManager#getLine1Number()
//		 */
//		public String getPhoneNumber() {
//			TelephonyManager mTelephonyMgr = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//			String number = mTelephonyMgr.getLine1Number();
//
//			//if using the emulator make a fake one
//			if(TextUtils.isEmpty(number))
//				number = "(123)4567890";		
//
//			return doSha1AndBase64Encryption(
//					number,
//					salt
//			);
//		}
//
//
//		/**
//		 * Gets the Android phone ID.
//		 * 
//		 * A 64-bit number (as a hex string) that is randomly 
//		 * generated on the device's first boot and should remain 
//		 * constant for the lifetime of the device. 
//		 * (The value may change if a factory reset is performed on 
//		 * the device.) 
//		 * 
//		 * Does not work in the Android Emulator
//		 * 
//		 * @return Encrypted DeviceId
//		 * 
//		 * @see Settings.System#ANDROID_ID
//		 */
//		public String getAndroidBootId() {
//			String id = Settings.System.getString(context.getContentResolver(), Settings.System.ANDROID_ID);
//
//			//if using the emulator make a fake one
//			if(TextUtils.isEmpty(id))
//				id = "987654321";
//
//			return doSha1AndBase64Encryption(id,salt);
//		}
//
//
//
//		/**
//		 * Returns the current date
//		 * 
//		 * @return Current Date
//		 */
//		public String getCurrentDate() {
//			Date now = new Date();
//			return now.toString();
//		}




		@Override
		public boolean isLicenseStillCurrent() {
			if(props == null) return false;
			if(!verifySignature()) return false;

			Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone("UTC") );
			Date now = calendar.getTime();

			Date expiry;
			try {
				long unixTimestamp = Long.valueOf( props.getProperty(Feature.EXPIRY.getKey()) );
				expiry = new Date( unixTimestamp*1000 );
			} catch (Exception e) {
				return false;
			}


			//check still current
			if( now.before(expiry) ) {
				return true;
			}   
			
			return false;

		}



		@Override
		public boolean isDeviceIdEqual() {
			//Log.d(TAG, "Checking Device ID");
			if(props == null) return false;
			if(!verifySignature()) return false;

			try {
				return Feature.DEVICE_ID.isEqual( props.getProperty( Feature.DEVICE_ID.getKey() ) );
			} catch (Exception e) {
				return false;
			}
		}


		@Override
		public boolean isAndroidBootIdEqual() {
			if(props == null) return false;
			if(!verifySignature()) return false;

			try {
				return Feature.ANDROID_BOOT_ID.isEqual( props.getProperty( Feature.ANDROID_BOOT_ID.getKey() ) );
			} catch (Exception e) {
				return false;
			}

		}


		@Override
		public boolean isPhoneNumberEqual() {
			if(props == null) return false;
			if(!verifySignature()) return false;

			try {
				return Feature.PHONE_NUMBER.isEqual( props.getProperty( Feature.PHONE_NUMBER.getKey() ) );
			} catch (Exception e) {
				return false;
			}
		}


		@Override
		public boolean isApplicationIdEqual(String appId) {
			if(props == null) return false;	
			if(appId==null) return false;
			if(!verifySignature()) return false;

			try {
				return props.getProperty(Feature.APPLICATION_ID.getKey()).equals(appId);
			} catch (Exception e) {
				return false;
			}
		}



		@Override
		public String getCreationTimestamp() {
			if(props == null) return "";
			if(!verifySignature()) return "";
			
			//			long milliseconds = Long.parseLong( props.getProperty(Feature.TIMESTAMP.getKey()) );
			//
			//			Date licenseDate = new Date(milliseconds);
			//			SimpleDateFormat formatter = new SimpleDateFormat("yyyy.MM.dd");
			//			String dateString = formatter.format(licenseDate);
		
			String timestamp =  props.getProperty(Feature.TIMESTAMP.getKey());
			
			return timestamp==null?"N/A":timestamp;
		}


		@Override
		public String getLicensingSystem() {
			if(props == null) return "";	
			if(!verifySignature()) return "";
			
			return props.getProperty(Feature.LICENSING_SYSTEM.getKey());
		}



		@Override
		public String getUsername() {
			if(props == null) return "";
			if(!verifySignature()) return "";
			
			return props.getProperty(Feature.USERNAME.getKey());
		}


		//		@Override
		//		public boolean checkValidLicense() {
		//			List<Feature> features = Feature.getEnabledFeatures();
		//			
		//			//Compare each feature
		//			for( Feature f : features ) {
		//				String property = this.props.getProperty(f.getKey());
		//				
		//				if( property==null )
		//					return false;
		//				
		//				if( !f.isEqual(property) ) {
		//					return false;
		//				}
		//			}
		//			
		//			return true;
		//		}



		@Override
		public long getExpiry() {
			long unixExpiry = Long.valueOf(props.getProperty(Feature.EXPIRY.getKey()));
			return unixExpiry*1000; //convert unix timestamp to milliseconds as required by Date()
		}


		/**
		 * JUnit Testing only
		 */
		String getJUnitDeviceId() {
			return props.getProperty(Feature.DEVICE_ID.getKey());
		}


		/**
		 * JUnit Testing only
		 */
		String getJUnitUsername() {
			return props.getProperty(Feature.USERNAME.getKey());
		}



	}

}
